package dfferber.xstream;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.Map.Entry;

import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;

public class PureJavaPojoReflectionProvider implements PojoReflectionProvider {
	/**
	 * Cache of properties known for relevant classes, indexed by class and property name. 
	 * */
	private transient Map<Class<?>, Map<String, PropertyDescriptor>> classToDescriptorList = new WeakHashMap<Class<?>, Map<String, PropertyDescriptor>>();
	
	@Override
	public boolean isPojo(Class<?> type) {
		return true;
	}
	
	@Override
	public PropertyDescriptor getPropertyDescriptor(Class<?> definedIn, String propertyName) {
		Map<String, PropertyDescriptor> item = discoverPropertyDescriptorsForClass(definedIn);
		PropertyDescriptor descriptor = item.get(propertyName);
		if (descriptor == null) {
			throw new XStreamException("Could not discover property " + definedIn + "." + propertyName);
		}
		return descriptor;
	}
	
	private Object readResolve() {
		this.classToDescriptorList = new WeakHashMap<Class<?>, Map<String, PropertyDescriptor>>();
		return this;
	}

	/** Queries and manages the cache of property descriptors. */
	private Map<String, PropertyDescriptor> discoverPropertyDescriptorsForClass(Class<?> definedIn) {
		Map<String, PropertyDescriptor> map = classToDescriptorList.get(definedIn);
		if (map == null) {
			BeanInfo beanInfo;
            try {
                beanInfo = Introspector.getBeanInfo(definedIn, Object.class);
            } catch (IntrospectionException e) {
                throw new ObjectAccessException("Cannot get BeanInfo of type " + definedIn.getName(), e);
            }
			PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
			map = new LinkedHashMap<String, PropertyDescriptor>();
			for (PropertyDescriptor descriptor : descriptors) {
				if (canStreamProperty(descriptor)) {
					map.put(descriptor.getName(), descriptor);
				}
			}
			classToDescriptorList.put(definedIn, map);
		}
		return map;
	}
	
    public boolean canStreamProperty(PropertyDescriptor descriptor) {
        return true;
    }

	@Override
	public void setProperty(Object object, String propertyName, Object value) {
		Method method = getPropertyDescriptor(object.getClass(), propertyName).getWriteMethod();
        validateMethodAccess(method);
        try {
        	method.invoke(object, new Object[]{value});
        } catch (IllegalArgumentException e) {
            throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
        } catch (IllegalAccessException e) {
            throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
        } catch (InvocationTargetException e) {
        	throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
		}
	}
	
	@Override
	public Object getProperty(Object object, String propertyName) {
		Method method = getPropertyDescriptor(object.getClass(), propertyName).getReadMethod();
        validateMethodAccess(method);
        try {
        	return method.invoke(object, new Object[]{});
        } catch (IllegalArgumentException e) {
            throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
        } catch (IllegalAccessException e) {
            throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
        } catch (InvocationTargetException e) {
        	throw new ObjectAccessException("Could not set property " + object.getClass() + "." + propertyName, e);
		}
	}

	protected void validateMethodAccess(Method method) {
		method.setAccessible(true);
	}

	@Override
	public void visitSerializableProperties(Object object, Visitor visitor) {
		Map<String, PropertyDescriptor> items = discoverPropertyDescriptorsForClass(object.getClass());
		for (Entry<String, PropertyDescriptor> entry : items.entrySet()) {
			String propertyName = entry.getKey();
			PropertyDescriptor descriptor = entry.getValue();
			Class<?> propertyType = descriptor.getPropertyType();
			Method writeMethod = descriptor.getWriteMethod();
			Method readMethod = descriptor.getReadMethod();
			if (readMethod == null) {
				continue;
			}
			Class<?> definedIn = descriptor.getReadMethod().getDeclaringClass();
			try {
				Object value = readMethod.invoke(object);
				boolean writeable = writeMethod != null;
				visitor.visit(propertyName, propertyType, definedIn, value, writeable);
			} catch (IllegalArgumentException e) {
				throw new ObjectAccessException("Could not visit " + definedIn + "." + propertyName, e);
			} catch (IllegalAccessException e) {
				throw new ObjectAccessException("Could not visit " + definedIn + "." + propertyName, e);
			} catch (InvocationTargetException e) {
				throw new ObjectAccessException("Could not visit " + definedIn + "." + propertyName, e);
			}

		}
	}

}
